# -*- coding:UTF-8 -*-

# 文档分类,文档分类是机器智能(machine intelligence)的一个应用,很有实用价值,而且现在越来越普及
# 1、文档过滤
#    可能的应用：根据邮件正文自动将收件箱中的邮件划分为社交类邮件和工作相关邮件
#                识别出要求自动回复的邮件,并将其自动转发给最适合的人员进行处理
# 2、实例:对某一RSS订阅源的内容自动过滤到不同的分类之中
# 3、原有分类器的缺陷：基于规则的过滤容易被用户绕开
#                      与场景有关，不同场景结果可能不一样

#处理流程：1、获取文章特征（通过分词的方法,将文字统一规格)
from pysqlite2 import dbapi2 as sqlite

import re
import math

#功能：从文本中提取特征
#流程：以任何非字母类字符为分隔符对文本进行划分,将文本拆分成一个个单词.这一过程
#      只留下真正的单词,并将这些单词全都转换成了小写形式
def getwords(doc):
    splitter = re.compile('\\W*')
    # 根据非字母字符进行单词拆分
    words = [s.lower() for s in splitter.split(doc) if len(s) > 2 and len(s) < 20]
    # 只返回一组不重复的单词
    return dict([(w, 1) for w in words])


def sampletrain(cl):
    cl.train('Nobody owns the water.', 'good')
    cl.train('the quick rabbit jumps fences', 'good')
    cl.train('buy pharmaceuticals now', 'bad')
    cl.train('make quick money at the online casino', 'bad')
    cl.train('the quick brown fox jumps', 'good')

# 功能：通过接受训练的方式来学习如何对文档进行分类.分类器掌握的文档及其正确
#       分类的样本越多,其预测的效果就越好，
# 分类器：从极为不确定的状态开始,随着分类器不断了解到那些特征对于分类而言更为重要
#         其确定性也在逐渐增加。
class classifier:
    def __init__(self, getfeatures, filename=None):
        # 统计特征/分类组合的数量
        # 变量fc将记录位于各分类中的不同特征的数量
        # {'python':{'bad':0,'good':6},'the':{'bad':3,'good':3}}        
        self.fc = {}
        # 统计每个分类中的文档数量,这一信息是我们稍后即将讨论的概率计算所需        
        self.cc = {}
        # 从即将被归类的内容项中提取特征来。
        self.getfeatures = getfeatures

    # 增加对特征/分类组合的计数值
    def incf(self, f, cat):
        self.fc.setdefault(f, {})
        self.fc[f].setdefault(cat, 0)
        self.fc[f][cat] += 1

    # 增加对某一分类的计数值
    def incc(self, cat):
        self.cc.setdefault(cat, 0)
        self.cc[cat] += 1

    # 某一特征出现于某一分类中的次数
    def fcount(self, f, cat):
        if f in self.fc and cat in self.fc[f]:
            return float(self.fc[f][cat])
        return 0.0

    # 属于某一分类的内容项数量
    def catcount(self, cat):
        if cat in self.cc:
            return float(self.cc[cat])
        return 0

    # 所有内容项的数量
    def totalcount(self):
        return sum(self.cc.values())

    # 所有分类的列表
    def categories(self):
        return self.cc.keys()
		
		#train方法接受一个内容项和一个分类项作为参数
		#1、它利用getfeatures函数,将内容项拆分为彼此独立的各个特征
		#2、调用incf函数,针对该分类为每个特征增加计数值
		#3、增加针对该分类的总计数值
    def train(self, item, cat):
        features = self.getfeatures(item)
        # 针对该分类为每个特征增加计数值
        for f in features:
            self.incf(f, cat)

        # 增加针对该分类的计数值
        self.incc(cat)

#==========计算概率============================
    def fprob(self, f, cat):
        if self.catcount(cat) == 0: return 0

        # 特征在分类中出现的总次数，除以分类中包含内容项的总数
        return self.fcount(f, cat) / self.catcount(cat)

#=========从一个合理的推测开始===========================
    def weightedprob(self, f, cat, prf, weight=1.0, ap=0.5):
        # 计算当前的概率值
        basicprob = prf(f, cat)

        # 统计特征在所有分类中出现的次数
        totals = sum([self.fcount(f, c) for c in self.categories()])

        # 计算加权平均
        bp = ((weight * ap) + (totals * basicprob)) / (weight + totals)

        return bp

    def setthreshold(self, cat, t):
        self.thresholds[cat] = t

    def getthreshold(self, cat):
        if cat not in self.thresholds: return 1.0
        return self.thresholds[cat]
		
		#
    def classify(self, item, default=None):
        probs = {}
        # 寻找概率最大的分类
        max = 0.0
        for cat in self.categories():
            probs[cat] = self.prob(item, cat)
            if probs[cat] > max:
                max = probs[cat]
                best = cat

        # 确保概率值超出域值*次大概率值
        for cat in probs:
            if cat == best: continue
            if probs[cat] * self.getthreshold(best) > probs[best]: return default
        return best

#===========朴素贝叶斯分类=============================
class naivebayes(classifier):
    def __init__(self, getfeatures):
        classifier.__init__(self, getfeatures)
        self.minimums = {}

    def docprob(self, item, cat):
        features = self.getfeatures(item)

        # 将所有特征的概率相乘
        p = 1
        for f in features: p *= self.weightedprob(f, cat, self.fprob)
        return p

    def prob(self, item, cat):
        catprob = self.catcount(cat) / self.totalcount()
        docprob = self.docprob(item, cat)
        return docprob * catprob

#=========费舍尔分类器===============================
class fisherclassifier(classifier):
    def __init__(self, getfeatures):
        classifier.__init__(self, getfeatures)
        self.thresholds = {}

    def cprob(self, f, cat):
        # 特征在该分类中出现的频率
        clf = self.fprob(f, cat)
        if clf == 0: return 0

        # 特征在所有分类中出现的频率
        freqsum = sum([self.fprob(f, c) for c in self.categories()])

        # 概率等于特征在该分类中出现的频率除以总体频率
        p = clf / (freqsum)

        return p

#===========将各概率值组合起来=================
    def fisherprob(self, item, cat):
        # 将所有概率值相乘
        p = 1
        features = self.getfeatures(item)
        for f in features:
            p *= (self.weightedprob(f, cat, self.cprob))

        # 取自然对数，并乘以-2
        fscore = -2 * math.log(p)

        # 利用倒置对数卡方函数求得概率
        return self.invchi2(fscore, len(features) * 2)

    def invchi2(self, chi, df):
        m = chi / 2.0
        sum = term = math.exp(-m)
        for i in range(1, df // 2):
            term *= m / i
            sum += term
        return min(sum, 1.0)

    def setminimum(self, cat, min):
        self.minimums[cat] = min

    def getminmum(self, cat):
        if cat not in self.minimums: return 0
        return self.minimums[cat]

    def classify(self, item, default=None):
        # 循环遍历并寻找最佳结果
        best = default
        max = 0.0
        for c in self.categories():
            p = self, fisherprob(item, c)
            # 确保其超过下限值
            if p > self.getminimum(c) and p > max:
                best = c
                max = p
            return best

#=======分类器持久化========================
    # 数据持久化
    def setdb(self, dbfile):
        self.con = sqlite.connect(dbfile)
        self.con.execute('create table if not exists fc(feature,category,count)')
        self.con.execute('create table if not exists cc(category,count)')

    def incf(self, f, cat):
        count = self.fcount(f, cat)
        if count == 0:
            self.con.execute("insert into fc values('%s','%s',1)" % (f, cat))
        else:
            self.con.execute("update fc set count=%d where feature='%s' and category='%s'" % (count + 1, f, cat))

    def fcount(self, f, cat):
        res = self.con.execute('select count from fc where feature="%s" and category="%s"' % (f, cat)).fetchone()
        if res == None:
            return 0
        else:
            return float(res[0])

    def incc(self, cat):
        count = self.catcount(cat)
        if count == 0:
            self.con.execute("insert into cc values ('%s',1)" % (cat))
        else:
            self.con.execute("update cc set count=%d where category='%s'" % (count + 1, cat))

    def catcount(self, cat):
        res = self.con.execute('select count from cc where category="%s"' % (cat)).fetchone()
        if res == None:
            return 0
        else:
            return float(res[0])

    def categories(self):
        cur = self.con.execute('select category from cc')
        return [d[0] for d in cur]

    def totalcount(self):
        res = self.con.execute('select sum(count) from cc').fetchone()
        if res == None: return 0
        return res[0]



